<html>
<body>

<div>edges2cats</div>
<div id="edges2cats"></div>

<div>edges2shoes</div>
<div id="edges2shoes"></div>

<div>edges2handbags</div>
<div id="edges2handbags"></div>

<div>facades</div>
<div id="facades"></div>

<script src="deeplearn-0.3.15.js"></script>
<script>

var editor_background = new Image()
editor_background.src = "editor.png"

var DEBUG = false
var SIZE = 256

var editors = []
var request_in_progress = false

function main() {
  var create_editor = function(config) {
    var editor = new Editor(config)
    var elem = document.getElementById(config.name)
    elem.appendChild(editor.view.ctx.canvas)
    editors.push(editor)
  }

  create_editor({
    name: "edges2cats",
    weights_url: "/models/edges2cats_AtoB.pict",
    mode: "line",
    clear: "#FFFFFF",
    colors: {
      line: "#000000",
      eraser: "#ffffff",
    },
    draw: "#000000",
    initial_input: "/edges2cats-input.png",
    initial_output: "/edges2cats-output.png",
    sheet_url: "/edges2cats-sheet.jpg",
  })

  create_editor({
    name: "edges2shoes",
    weights_url: "/models/edges2shoes_AtoB.pict",
    mode: "line",
    clear: "#FFFFFF",
    colors: {
      line: "#000000",
      eraser: "#ffffff",
    },
    draw: "#000000",
    initial_input: "/edges2shoes-input.png",
    initial_output: "/edges2shoes-output.png",
    sheet_url: "/edges2shoes-sheet.jpg",
  })

  create_editor({
    name: "edges2handbags",
    weights_url: "/models/edges2handbags_AtoB.pict",
    mode: "line",
    clear: "#FFFFFF",
    colors: {
      line: "#000000",
      eraser: "#ffffff",
    },
    draw: "#000000",
    initial_input: "/edges2handbags-input.png",
    initial_output: "/edges2handbags-output.png",
    sheet_url: "/edges2handbags-sheet.jpg",
  })

  create_editor({
    name: "facades",
    weights_url: "/models/facades_BtoA.pict",
    mode: "rect",
    colors: {
      background: "#0006d9",
      wall: "#0d3dfb",
      door: "#a50000",
      "window": "#0075ff",
      "window sill": "#68f898",
      "window head": "#1dffdd",
      shutter: "#eeed28",
      balcony: "#b8ff38",
      trim: "#ff9204",
      cornice: "#ff4401",
      column: "#f60001",
      entrance: "#00c9ff",
    },
    clear: "#0d3dfb",
    draw: "#0075ff",
    initial_input: "/facades-input.png",
    initial_output: "/facades-output.png",
    sheet_url: "/facades-sheet.jpg",
  })

  window.requestAnimationFrame(frame)
}
window.onload = main

function render() {
  for (var i = 0; i < editors.length; i++) {
    editors[i].render()
  }
}


// model

var weights_cache = {}
function fetch_weights(path, progress_cb) {
  return new Promise(function(resolve, reject) {
    if (path in weights_cache) {
      resolve(weights_cache[path])
      return
    }

    var xhr = new XMLHttpRequest()
    xhr.open("GET", path, true)
    xhr.responseType = "arraybuffer"

    xhr.onprogress = function(e) {
      progress_cb(e.loaded, e.total)
    }

    xhr.onload = function(e) {
      if (xhr.status != 200) {
        reject("missing model")
        return
      }
      var buf = xhr.response
      if (!buf) {
        reject("invalid arraybuffer")
        return
      }

      var parts = []
      var offset = 0
      while (offset < buf.byteLength) {
        var b = new Uint8Array(buf.slice(offset, offset+4))
        offset += 4
        var len = (b[0] << 24) + (b[1] << 16) + (b[2] << 8) + b[3]
        parts.push(buf.slice(offset, offset + len))
        offset += len
      }

      var shapes = JSON.parse((new TextDecoder("utf8")).decode(parts[0]))
      var index = new Float32Array(parts[1])
      var encoded = new Uint8Array(parts[2])

      // decode using index
      var arr = new Float32Array(encoded.length)
      for (var i = 0; i < arr.length; i++) {
        arr[i] = index[encoded[i]]
      }

      var weights = {}
      var offset = 0
      for (var i = 0; i < shapes.length; i++) {
        var shape = shapes[i].shape
        var size = shape.reduce((total, num) => total * num)
        var values = arr.slice(offset, offset+size)
        var dlarr = dl.Array1D.new(values, "float32")
        weights[shapes[i].name] = dlarr.reshape(shape)
        offset += size
      }
      weights_cache[path] = weights
      resolve(weights)
    }
    xhr.send(null)
  })
}

function model(input, weights) {
  const math = dl.ENV.math

  function preprocess(input) {
    return math.subtract(math.multiply(input, dl.Scalar.new(2)), dl.Scalar.new(1))
  }

  function deprocess(input) {
    return math.divide(math.add(input, dl.Scalar.new(1)), dl.Scalar.new(2))
  }

  function batchnorm(input, scale, offset) {
    var moments = math.moments(input, [0, 1])
    const varianceEpsilon = 1e-5
    return math.batchNormalization3D(input, moments.mean, moments.variance, varianceEpsilon, scale, offset)
  }

  function conv2d(input, filter, bias) {
    return math.conv2d(input, filter, bias, [2, 2], "same")
  }

  function deconv2d(input, filter, bias) {
    var convolved = math.conv2dTranspose(input, filter, [input.shape[0]*2, input.shape[1]*2, filter.shape[2]], [2, 2], "same")
    var biased = math.add(convolved, bias)
    return biased
  }

  var preprocessed_input = preprocess(input)

  var layers = []

  var filter = weights["generator/encoder_1/conv2d/kernel"]
  var bias = weights["generator/encoder_1/conv2d/bias"]
  var convolved = conv2d(preprocessed_input, filter, bias)
  layers.push(convolved)

  for (var i = 2; i <= 8; i++) {
    var scope = "generator/encoder_" + i.toString()
    var filter = weights[scope + "/conv2d/kernel"]
    var bias = weights[scope + "/conv2d/bias"]
    var layer_input = layers[layers.length - 1]
    var rectified = math.leakyRelu(layer_input, 0.2)
    var convolved = conv2d(rectified, filter, bias)
    var scale = weights[scope + "/batch_normalization/gamma"]
    var offset = weights[scope + "/batch_normalization/beta"]
    var normalized = batchnorm(convolved, scale, offset)
    layers.push(normalized)
  }

  for (var i = 8; i >= 2; i--) {
    if (i == 8) {
      var layer_input = layers[layers.length - 1]
    } else {
      var skip_layer = i - 1
      var layer_input = math.concat3D(layers[layers.length - 1], layers[skip_layer], 2)
    }
    var rectified = math.relu(layer_input)
    var scope = "generator/decoder_" + i.toString()
    var filter = weights[scope + "/conv2d_transpose/kernel"]
    var bias = weights[scope + "/conv2d_transpose/bias"]
    var convolved = deconv2d(rectified, filter, bias)
    var scale = weights[scope + "/batch_normalization/gamma"]
    var offset = weights[scope + "/batch_normalization/beta"]
    var normalized = batchnorm(convolved, scale, offset)
    // missing dropout
    layers.push(normalized)
  }

  var layer_input = math.concat3D(layers[layers.length - 1], layers[0], 2)
  var rectified = math.relu(layer_input)
  var filter = weights["generator/decoder_1/conv2d_transpose/kernel"]
  var bias = weights["generator/decoder_1/conv2d_transpose/bias"]
  var convolved = deconv2d(rectified, filter, bias)
  var rectified = math.tanh(convolved)
  layers.push(rectified)

  var output = layers[layers.length - 1]
  var deprocessed_output = deprocess(output)

  return deprocessed_output
}


// editor

function Editor(config) {
  this.config = config
  this.view = new View(this.config.name, 800, 400)

  this.buffers = []

  this.buffer = createContext(SIZE, SIZE, SCALE)
  this.buffer.fillStyle = this.config.clear
  this.buffer.fillRect(0, 0, SIZE, SIZE)

  var image = new Image()
  image.src = this.config.initial_input
  image.onload = () => {
    this.buffer.drawImage(image, 0, 0)
  }

  this.output = createContext(SIZE, SIZE, 1)
  var output = new Image()
  output.src = this.config.initial_output
  output.onload = () => {
    this.output.drawImage(output, 0, 0)
  }

  this.progress = null
  this.last_failure = null

  this.sheet_loaded = false
  this.sheet = new Image()
  this.sheet.src = this.config.sheet_url
  this.sheet.onload = () => {
    this.sheet_loaded = true
    update()
  }
  this.sheet_index = 0
}

Editor.prototype = {
  push_buffer: function() {
    this.buffers.push(this.buffer)
    var buffer = createContext(SIZE, SIZE, SCALE)
    buffer.save()
    buffer.scale(1/SCALE, 1/SCALE)
    buffer.drawImage(this.buffer.canvas, 0, 0)
    buffer.restore()
    this.buffer = buffer
  },
  pop_buffer: function() {
    if (this.buffers.length == 0) {
      return
    }
    this.buffer = this.buffers.pop()
  },
  render: function() {
    var v = this.view

    v.ctx.clearRect(0, 0, v.f.width, v.f.height)
    v.ctx.save()
    v.ctx.scale(1/SCALE, 1/SCALE)
    v.ctx.drawImage(editor_background, 0, 0)
    v.ctx.restore()

    v.frame("tools", 8, 41, 100, 250, () => {
      var i = 0
      for (var name in this.config.colors) {
        var color = this.config.colors[name]
        v.frame("color_selector", 0, i*21, v.f.width, 20, () => {
          if (v.contains(mouse_pos)) {
            cursor_style = "pointer"
          }

          if (mouse_released && v.contains(mouse_pos)) {
            this.config.draw = color
            update()
          }

          if (this.config.draw == color) {
            v.ctx.save()
            var radius = 5
            v.ctx.beginPath()
            v.ctx.moveTo(radius, 0)
            var sides = [v.f.width, v.f.height, v.f.width, v.f.height]
            for (var i = 0; i < sides.length; i++) {
              var side = sides[i]
              v.ctx.lineTo(side - radius, 0)
              v.ctx.arcTo(side, 0, side, radius, radius)
              v.ctx.translate(side, 0)
              v.ctx.rotate(90 / 180 * Math.PI)
            }
            v.ctx.fillStyle = rgba([0.5, 0.5, 0.5, 1.0])
            v.ctx.stroke()
            v.ctx.restore()
            v.ctx.font = "bold 8pt Arial"
          } else {
            v.ctx.font = "8pt Arial"
          }

          v.ctx.fillText(name, v.f.width - v.ctx.measureText(name).width - 26, 10)

          v.frame("color", v.f.width-25, 0, 20, 20, () => {
            v.ctx.beginPath()
            v.ctx.fillStyle = "#666666"
            v.ctx.arc(10, 10, 9, 0, 2 * Math.PI, false)
            v.ctx.fill()
            v.ctx.beginPath()
            v.ctx.fillStyle = color
            v.ctx.arc(10, 10, 8, 0, 2 * Math.PI, false)
            v.ctx.fill()
          })
        })
        i++
      }
    })

    v.frame("output", 530, 40, 256, 256, () => {
      v.ctx.drawImage(this.output.canvas, 0, 0)
    })

    v.frame("input", 140, 40, 256, 256+40, () => {
      v.frame("image", 0, 0, 256, 256, () => {
        v.ctx.drawImage(this.buffer.canvas, 0, 0, v.f.width, v.f.height)

        if (v.contains(mouse_pos)) {
          cursor_style = "crosshair"
          if (this.config.mode == "line" && this.config.draw == "#ffffff") {
            // eraser tool
            cursor_style = "url(/eraser.png) 8 8, auto"
          }
        }

        if (this.config.mode == "line") {
          // this is to make undo work with lines, rather than removing only single frame line segments
          var drag_from_outside = mouse_down && v.contains(mouse_pos) && !v.contains(last_mouse_pos)
          var start_inside = mouse_pressed && v.contains(mouse_pos)
          if (drag_from_outside || start_inside) {
            this.push_buffer()
          }

          if (mouse_down && v.contains(mouse_pos)) {
            var last = v.relative(last_mouse_pos)
            var cur = v.relative(mouse_pos)
            this.buffer.beginPath()
            this.buffer.lineCap = "round"
            this.buffer.strokeStyle = this.config.draw
            if (this.config.draw == "#ffffff") {
              // eraser mode
              this.buffer.lineWidth = 15
            } else {
              this.buffer.lineWidth = 1
            }
            this.buffer.moveTo(last.x, last.y)
            this.buffer.lineTo(cur.x, cur.y)
            this.buffer.stroke()
            this.buffer.closePath()
          }
        } else {
          if (v.contains(drag_start)) {
            var start = v.relative(drag_start)
            var end = v.relative(mouse_pos)
            var width = end.x - start.x
            var height = end.y - start.y
            if (mouse_down) {
              v.ctx.save()
              v.ctx.rect(0, 0, v.f.width, v.f.height)
              v.ctx.clip()
              v.ctx.fillStyle = this.config.draw
              v.ctx.fillRect(start.x, start.y, width, height)
              v.ctx.restore()
            } else if (mouse_released) {
              this.push_buffer()
              this.buffer.fillStyle = this.config.draw
              this.buffer.fillRect(start.x, start.y, width, height)
              v.ctx.drawImage(this.buffer.canvas, 0, 0, v.f.width, v.f.height)
            }
          }
        }
      })
    })

    v.frame("process_button", 461 - 32, 148, 32*2, 40, () => {
      if (this.progress != null) {
        v.ctx.font = "12px Arial"
        v.ctx.fillStyle = "#000"
        var s = "downloading"
        v.ctx.fillText(s, (v.f.width - v.ctx.measureText(s).width)/2, 5)
        s = "model"
        v.ctx.fillText(s, (v.f.width - v.ctx.measureText(s).width)/2, 15)

        v.frame("progress_bar", 0, 25, v.f.width, 15, () => {
          v.ctx.fillStyle = "#f92672"
          v.ctx.fillRect(0, 0, v.f.width * this.progress, v.f.height)
        })
      } else if (request_in_progress) {
        do_button(v, "running")
      } else {
        if (do_button(v, "process")) {
          if (request_in_progress) {
            console.log("request already in progress")
            return
          }
          request_in_progress = true
          this.last_failure = null

          this.progress = 0
          progress_cb = (retrieved, total) => {
            this.progress = retrieved/total
            update()
          }

          fetch_weights(this.config.weights_url, progress_cb).then((weights) => {
            this.progress = null
            update()
            // delay a short period of time so that UI updates before the model uses all the CPU
            delay(() => {
              // var g = new dl.Graph()

              var convert = createContext(SIZE, SIZE, 1)
              convert.drawImage(this.buffer.canvas, 0, 0, convert.canvas.width, convert.canvas.height)
              var input_uint8_data = convert.getImageData(0, 0, SIZE, SIZE).data
              var input_float32_data = Float32Array.from(input_uint8_data, (x) => x / 255)

              console.time('render')
              const math = dl.ENV.math
              math.startScope()
              var input_rgba = dl.Array3D.new([SIZE, SIZE, 4], input_float32_data, "float32")
              var input_rgb = math.slice3D(input_rgba, [0, 0, 0], [SIZE, SIZE, 3])

              var output_rgb = model(input_rgb, weights)

              var alpha = dl.Array3D.ones([SIZE, SIZE, 1])
              var output_rgba = math.concat3D(output_rgb, alpha, 2)

              output_rgba.getValuesAsync().then((output_float32_data) => {
                var output_uint8_data = Uint8ClampedArray.from(output_float32_data, (x) => x * 255)
                this.output.putImageData(new ImageData(output_uint8_data, SIZE, SIZE), 0, 0)
                math.endScope()
                console.timeEnd('render')
                request_in_progress = false
                update()
              })
            })
          }, (e) => {
            this.last_failure = e
            this.progress = null
            request_in_progress = false
            update()
          })
        }
      }
    })

    v.frame("undo_button", 192-32, 310, 64, 40, () => {
      if (do_button(v, "undo")) {
        this.pop_buffer()
        update()
      }
    })

    v.frame("clear_button", 270-32, 310, 64, 40, () => {
      if (do_button(v, "clear")) {
        this.buffers = []
        this.buffer.fillStyle = this.config.clear
        this.buffer.fillRect(0, 0, SIZE, SIZE)
        this.output.fillStyle = "#FFFFFF"
        this.output.fillRect(0, 0, SIZE, SIZE)
      }
    })

    if (this.sheet_loaded) {
      v.frame("random_button", 347-32, 310, 64, 40, () => {
        if (do_button(v, "random")) {
          // pick next sheet entry
          this.buffers = []
          var y_offset = this.sheet_index * SIZE
          this.buffer.drawImage(this.sheet, 0, y_offset, SIZE, SIZE, 0, 0, SIZE, SIZE)
          this.output.drawImage(this.sheet, SIZE, y_offset, SIZE, SIZE, 0, 0, SIZE, SIZE)
          this.sheet_index = (this.sheet_index + 1) % (this.sheet.height / SIZE)
          update()
        }
      })
    }

    v.frame("save_button", 655-32, 310, 64, 40, () => {
      if (do_button(v, "save")) {
        // create a canvas to hold the part of the canvas that we wish to store
        var x = 125 * SCALE
        var y = 0
        var width = 800 * SCALE - x
        var height = 310 * SCALE - y
        var convert = createContext(width, height, 1)
        convert.drawImage(v.ctx.canvas, x, y, width, height, 0, 0, convert.canvas.width, convert.canvas.height)
        var data_b64 = convert.canvas.toDataURL("image/png").replace(/^data:image\/png;base64,/, "")
        var data = b64_to_bin(data_b64)
        var blob = new Blob([data], {type: "application/octet-stream"})
        var url = window.URL.createObjectURL(blob)
        var a = document.createElement("a")
        a.href = url
        a.download = "pix2pix.png"
        // use createEvent instead of .click() to work in firefox
        // also can"t revoke the object url because firefox breaks
        var event = document.createEvent("MouseEvents")
        event.initEvent("click", true, true)
        a.dispatchEvent(event)
        // safari doesn"t work at all
      }
    })

    if (this.last_failure != null) {
      v.frame("server_error", 50, 350, v.f.width, 50, () => {
        v.ctx.font = "20px Arial"
        v.ctx.fillStyle = "red"
        v.center_text(fmt("error %s", this.last_failure))
      })
    }
  },
}

// utility

function createContext(width, height, scale) {
  var canvas = document.createElement("canvas")
  canvas.width = width * scale
  canvas.height = height * scale
  stylize(canvas, {
    width: fmt("%dpx", width),
    height: fmt("%dpx", height),
    margin: "10px auto 10px auto",
  })
  var ctx = canvas.getContext("2d")
  ctx.scale(scale, scale)
  return ctx
}

function b64_to_bin(str) {
  var binstr = atob(str)
  var bin = new Uint8Array(binstr.length)
  for (var i = 0; i < binstr.length; i++) {
    bin[i] = binstr.charCodeAt(i)
  }
  return bin
}

function delay(fn) {
  setTimeout(fn, 0)
}

function default_format(obj) {
  if (typeof(obj) === "string") {
    return obj
  } else {
    return JSON.stringify(obj)
  }
}

function fmt() {
  if (arguments.length === 0) {
    return "error"
  }

  var format = arguments[0]
  var output = ""

  var arg_index = 1
  var i = 0

  while (i < format.length) {
    var c = format[i]
    i++

    if (c != "%") {
      output += c
      continue
    }

    if (i === format.length) {
      output += "%!(NOVERB)"
      break
    }

    var flag = format[i]
    i++

    var pad_char = " "

    if (flag == "0") {
      pad_char = "0"
    } else {
      // not a flag
      i--
    }

    var width = 0
    while (format[i] >= "0" && format[i] <= "9") {
      width *= 10
      width += parseInt(format[i], 10)
      i++
    }

    var f = format[i]
    i++

    if (f === "%") {
      output += "%"
      continue
    }

    if (arg_index === arguments.length) {
      output += "%!" + f + "(MISSING)"
      continue
    }

    var arg = arguments[arg_index]
    arg_index++

    var o = null

    if (f === "v") {
      o = default_format(arg)
    } else if (f === "s" && typeof(arg) === "string") {
      o = arg
    } else if (f === "T") {
      o = typeof(arg)
    } else if (f === "d" && typeof(arg) === "number") {
      o = arg.toFixed(0)
    } else if (f === "f" && typeof(arg) === "number") {
      o = arg.toString()
    } else if (f === "x" && typeof(arg) === "number") {
      o = Math.round(arg).toString(16)
    } else if (f === "t" && typeof(arg) === "boolean") {
      if (arg) {
        o = "true"
      } else {
        o = "false"
      }
    } else {
      output += "%!" + f + "(" + typeof(arg) + "=" + default_format(arg) + ")"
    }

    if (o !== null) {
      if (o.length < width) {
        output += Array(width - o.length + 1).join(pad_char)
      }
      output += o
    }
  }

  if (arg_index < arguments.length) {
    output += "%!(EXTRA "
    while (arg_index < arguments.length) {
      var arg = arguments[arg_index]
      output += typeof(arg) + "=" + default_format(arg)
      if (arg_index < arguments.length - 1) {
        output += ", "
      }
      arg_index++
    }
    output += ")"
  }

  return output
}


// immediate mode UI

var SCALE = 2

var updated = true
var frame_rate = 0
var now = new Date()
var last_frame = new Date()
var animations = {}
var values = {}

var cursor_style = null
var mouse_pos = [0, 0]
var last_mouse_pos = [0, 0]
var drag_start = [0, 0]
var mouse_down = false
var mouse_pressed = false
var mouse_released = false

if (DEBUG) {
  var fps_elem = document.createElement("div")
  stylize(fps_elem, {
    width: "300px",
    height: "20px",
    margin: "5px",
    fontFamily: "Monaco",
    fontSize: "12px",
    position: "absolute",
    top: fmt("%dpx", 10),
    right: fmt("%dpx", 10),
  })
  document.body.insertBefore(fps_elem, document.body.firstChild)

  var status_elem = document.createElement("div")
  stylize(status_elem, {
    width: "10px",
    height: "10px",
    margin: "5px",
    position: "absolute",
    top: fmt("%dpx", 10),
    left: fmt("%dpx", 10),
  })
  document.body.insertBefore(status_elem, document.body.firstChild)
}

function View(name, width, height) {
  this.ctx = createContext(width, height, SCALE)
	// https://developer.apple.com/library/safari/documentation/AudioVideo/Conceptual/HTML-canvas-guide/AddingText/AddingText.html
  this.ctx.textBaseline = "middle"
  this.frames = [{name: name, offset_x: 0, offset_y: 0, width: width, height: height}]
  this.f = this.frames[0]
}

View.prototype = {
  push_frame: function(name, x, y, width, height) {
    this.ctx.save()
    this.ctx.translate(x, y)
  	var current = this.frames[this.frames.length - 1]
  	var next = {name: name, offset_x: current.offset_x + x, offset_y: current.offset_y + y, width: width, height: height}
  	this.frames.push(next)
    this.f = next
  },
  pop_frame: function() {
    this.ctx.restore()
    this.frames.pop()
    this.f = this.frames[this.frames.length - 1]
  },
  frame: function(name, x, y, width, height, func) {
    this.push_frame(name, x, y, width, height)
    func()
    this.pop_frame()
  },
  frame_path: function() {
    var parts = []
    for (var i = 0; i < this.frames.length; i++) {
      parts.push(this.frames[i].name)
    }
    return parts.join(".")
  },
  relative: function(pos) {
    // adjust x and y relative to the top left corner of the canvas
    // then adjust relative to the current frame
    var rect = this.ctx.canvas.getBoundingClientRect()
    return {x: pos.x - rect.left - this.f.offset_x, y: pos.y - rect.top - this.f.offset_y}
  },
  contains: function(pos) {
    // first check that position is inside canvas container
    var rect = this.ctx.canvas.getBoundingClientRect()
    if (pos.x < rect.left || pos.x > rect.left + rect.width || pos.y < rect.top || pos.y > rect.top + rect.height) {
      return false
    }
    // translate coordinates to the current frame
    var rel = this.relative(pos)
    return 0 < rel.x && rel.x < this.f.width && 0 < rel.y && rel.y < this.f.height
  },
  put_image_data: function(d, x, y) {
    this.ctx.putImageData(d, (x + this.f.offset_x) * SCALE, (y + this.f.offset_y) * SCALE)
  },
  center_text: function(s) {
    this.ctx.fillText(s, (this.f.width - this.ctx.measureText(s).width)/2, this.f.height/2)
  },
}

function do_button(v, text) {
  name = v.frame_path()

  if (v.contains(mouse_pos)) {
    cursor_style = "pointer"
  }

  if (request_in_progress) {
    animate(name, parse_color("#aaaaaaFF"), 100)
  } else if (mouse_down && v.contains(mouse_pos)) {
    animate(name, parse_color("#FF0000FF"), 50)
  } else {
    if (v.contains(mouse_pos)) {
      animate(name, parse_color("#f477a5FF"), 100)
    } else {
      animate(name, parse_color("#f92672FF"), 100)
    }
  }

  v.ctx.save()
  var radius = 5
  v.ctx.beginPath()
  v.ctx.moveTo(radius, 0)
  var sides = [v.f.width, v.f.height, v.f.width, v.f.height]
  for (var i = 0; i < sides.length; i++) {
    var side = sides[i]
    v.ctx.lineTo(side - radius, 0)
    v.ctx.arcTo(side, 0, side, radius, radius)
    v.ctx.translate(side, 0)
    v.ctx.rotate(90 / 180 * Math.PI)
  }
  v.ctx.fillStyle = rgba(calculate(name))
  v.ctx.fill()
  v.ctx.restore()

  v.ctx.font = "16px Arial"
  v.ctx.fillStyle = "#f8f8f8"
  v.center_text(text)

  if (request_in_progress) {
    return false
  }

  return mouse_released && v.contains(mouse_pos) && v.contains(drag_start)
}

function stylize(elem, style) {
  for (var key in style) {
    elem.style[key] = style[key]
  }
}

function update() {
  updated = true
}

function frame() {
  var raf = window.requestAnimationFrame(frame)

  if (!updated && Object.keys(animations).length == 0) {
    if (DEBUG) {
      status_elem.style.backgroundColor = "black"
    }
    return
  }
  if (DEBUG) {
    status_elem.style.backgroundColor = "red"
  }

  now = new Date()
  cursor_style = null
  updated = false

  try {
    render()
  } catch (e) {
    window.cancelAnimationFrame(raf)
    throw e
  }

  if (cursor_style == null) {
    document.body.style.cursor = "default"
  } else {
    document.body.style.cursor = cursor_style
  }

  if (DEBUG) {
    var decay = 0.9
    var current_frame_rate = 1 / ((now - last_frame) / 1000)
    frame_rate = frame_rate * decay + current_frame_rate * (1 - decay)
    fps_elem.textContent = fmt("fps: %d", frame_rate)
  }

  last_frame = now
  last_mouse_pos = mouse_pos
  mouse_pressed = false
  mouse_released = false
}

function array_equal(a, b) {
  if (a.length != b.length) {
    return false
  }

  for (var i = 0; i < a.length; i++) {
    if (a[i] != b[i]) {
      return false
    }
  }
  return true
}

function animate(name, end, duration) {
  if (values[name] == undefined) {
    // no value has been set for this element, set it immediately
    values[name] = end
    return
  }

  var v = calculate(name)
  if (array_equal(v, end)) {
    return
  }
  if (duration == 0) {
    delete animations[name]
    values[name] = end
    return
  }
  var a = animations[name]
  if (a != undefined && array_equal(a.end, end)) {
    return
  }
  animations[name] = {time: now, start: v, end: end, duration: duration}
}

function calculate(name) {
  if (values[name] == undefined) {
    throw "calculate used before calling animate"
  }

  var a = animations[name]
  if (a != undefined) {
    // update value
    var t = Math.min((now - a.time)/a.duration, 1.0)
    t = t*t*t*(t*(t*6 - 15) + 10) // smootherstep
    var result = []
    for (var i = 0; i < a.start.length; i++) {
      result[i] = a.start[i] + (a.end[i] - a.start[i]) * t
    }
    if (t == 1.0) {
      delete animations[name]
    }
    values[name] = result
  }
  return values[name]
}

function rgba(v) {
  return fmt("rgba(%d, %d, %d, %f)", v[0] * 255, v[1] * 255, v[2] * 255, v[3])
}

var parse_color = function(c) {
	return [
		parseInt(c.substr(1,2), 16) / 255,
		parseInt(c.substr(3,2), 16) / 255,
		parseInt(c.substr(5,2), 16) / 255,
    parseInt(c.substr(7,2), 16) / 255,
	]
}

document.addEventListener("mousemove", function(e) {
  mouse_pos = {x: e.clientX, y: e.clientY}
  update()
})

document.addEventListener("mousedown", function(e) {
  drag_start = {x: e.clientX, y: e.clientY}
  mouse_down = true
  mouse_pressed = true
  update()
})

document.addEventListener("mouseup", function(e) {
  mouse_down = false
  mouse_released = true
  update()
})

</script>

</body>
</html>
